# h1. fnfal
# Functional Fixed Arity Language
# josh 2011-09-19

# Many times I have needed to provide some small amount of
# scriptability for my programs to users reasonable well versed in
# command line usage, but ignorant of the intricacies of Python
# programming. This little library is a first stab at simple
# logo-style fixed arity language.

# h2. Front Matter

# h3. Imports

import sys, os, re, string, cStringIO, pprint


# h2. Utilities

def intp(data): return isinstance(data, int)

def stringp(data): # I hate unicode...
    return isinstance(data, basestring)

def tuplep(data): return isinstance(data, tuple)

def listp(data): return isinstance(data, list)

def hashp(data): return isinstance(data, dict)


# An 'atom' is a word, or name, which admits to no other
# operations. It's immutable and I use it for all unquoted objects in
# the parse stream.

class atom(object):
    def __init__(my, value): my.value = value

    def __repr__(my): return "atom(%s)" % my.value
    

# The @word@ function parses out a single word (marked by an optional
# terminating delimiter (or set of delimeters)) from an input
# stream. The input stream should be a "file-like" object, which means
# an actual file or something derived from StringIO (Python's closest
# equivalent to REBOL series types).

def word(stream, stop=None):
    "( stream stop -- (chunk, rest)) Parse next word from input stream."

    if not stop: stop = " \t\n\r"
    chunk = ""
    escaped = False
    while True:
        c = stream.read(1)
        if not c: break

        if   escaped:
            chunk += c
            escaped = False
        elif c in stop:
            if len(chunk): break # End of word.
            # Otherwise consume leading whitespace.
        else:
            if not len(chunk):
                if   '"' == c:
                    chunk = '"%s"' % word(stream, '"')
                    c = stream.read(1) # Get rid of trailing quote.
                    break # We've got the string, so we're done.
                elif ";" == c:
                    word(stream, "\n")
                else: chunk += c
            else:
                if "\\" == c: escaped = True
                chunk += c

    return chunk


# In a Forth-like spirit, I don't want any special words. However,
# because of the prefix/infix notation, I must unavoidably have
# grouping operators. So, I have two: "[]" (list) indicates an
# unevaluated list, while the contents of "()" (tuple) are to be
# processed immediately. This allows algebraic grouping such that my
# target audience should be used to. Everything else becomes a string,
# a number or an 'atom'.

def collect(stream):
    ast = [[]]
    clause = ast[-1]
    
    while True:
        w = word(stream)
        if not w: break

        if   "[" == w or "(" == w:
            clause = []
            ast.append(clause)
        elif "]" == w or ")" == w:
            x = ast.pop()
            if ")" == w: x = tuple(x)
            ast[-1].append(x)
            clause = ast[-1]
        elif w.startswith('"'):
            clause.append(w[1:-1])
        else:
            try: clause.append(float(w))
            except: clause.append(atom(w))

    assert 1 == len(ast), "List not closed."
    return ast[0]
            

# h2. Language Kernel

class fnfal(object):

# I statically define the @__d@ dictionary to avoid arguments with
# *attr functions.

    __d = {}

# To initialize the interpreter, I load the dictionary with all the
# tagged words in the class. The dectionary format is docstring, arity
# and xt. When I have the capscity to define words, they will use a
# list instead of xt. I add extra words with special naming using the
# "opt()" method.
    
    def __init__(my):
        for a in dir(my):
            if not a.startswith("fn"): continue
            my.opt(getattr(my, a), a[2:])

        my.opt(my.xtype, "type?")


    def opt(my, xt, *names):
        for name in names: my.__d[name] = (
            xt.__doc__,
            xt.func_code.co_argcount - 1,
            xt)


# h3. Tokenization and Parsing

# Note: This needs to handle tuple evaluation (remember, lists are
# deferred, tuples are immediate). Also needs to understand variables.

    def fndo(my, chunks):
        results = []
        
        while len(chunks):
            chunk = chunks.pop(0)

            if isinstance(chunk, atom):
                try: doc, arity, xt = my.__d[chunk.value]
                except ValueError:
                    raise SyntaxError, "I don't know how to %s." % chunk.value
                args = []
                for i in range(arity): args.append(my.fndo(chunks)[0])
                results.append(apply(xt, args))
            else: results.append(chunk)

            return results


    def fneval(my, text):
        "( text -- results) Execute code in text."

        if stringp(text): text = cStringIO.StringIO(text)
        return my.fndo(collect(text))
            

# h3. Memory

    def fnset(my, key, value):
        my.__dictionary[key] = value
        return value

    def xtype(my, x): "( x -- type) Get type of x."; return type(x)


# h3. Math Operations

    def fnadd(my, a, b): return a + b

    def fnsub(my, a, b): return a - b

    def fnmul(my, a, b): return a * b

    def fndiv(my, a, b): return a / b

    def fnmod(my, a, b): return a % b


# h3. Control Flow

    def fnrepeat(my, times, body):
        for i in range(times): my.fndo(body)

# In concept, this is what the fn word should do. This code will
# almost certainly not work.

    def fnfn(my, name, args, body):
        my.__d[name] = ("Documentation", len(args), body)
    

# h3. I/O Operations

    def fnprint(my, msg):
        "( msg -- msg) Print msg to stdout."
        
        print msg
        return msg


class model(fnfal):
    def __init__(my):
        fnfal.__init__(my)

    def main(my):
        my.fneval(collect(sys.argv[1:])) # XXX Need to modify collect
                                         # to accept pre-parsed words.
        

if "__main__" == __name__:

# Tests

    text = cStringIO.StringIO('This ; Once upon a time.\nGork "Hack a day." 75 "Hack \\"a\\" day."')
    assert "This" == word(text)
    assert "Gork" == word(text)
    assert '"Hack a day."' == word(text)
    assert "75" == word(text)
    assert '"Hack \\"a\\" day."' == word(text)

    s = cStringIO.StringIO("""\
def "fib" [
    "( n -- fn) Compute Fibonnacci number."
    n
] [
    ifelse  any [ 0 = n  1 = n ]
        n ; Return unmodified argument.
        [ fib ( n - 1 ) + fib ( n - 2 ) ]
]
""")
    # pprint.pprint(collect(s))
    assert 4 == len(collect(s)), "Should be four clauses in definition."

    f = fnfal()
    assert [8,] == f.fneval("8"), "Fndo: Numeric constant."
    assert [8,] == f.fneval("add 5 3"), "Fndo: Add."
    assert [8,] == f.fneval("print add 4 4"), "Fndo: Print."
    assert [type(0.0),] == f.fneval("type? add 8 3"), "Fndo: Type result."
